﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using CoreEx.Common.Extensions;
using CoreEx.Database.Metadata;
using LinFu.IoC.Configuration;

namespace CoreEx.Database.MetaData.Implementation
{
    [Implements(typeof(ITypeWriter))]
    public class TypeWriter : ITypeWriter
    {
        private int _memberIndex;


        public void Generate(TableInfo tableInfo, string nameSpace, TextWriter textWriter, bool generateEquals)
        {
            _memberIndex = 0;
            var foreignTables = GetOneToManyForeignTables(tableInfo);
            var foreignKeyColumns = GetForeignKeyColumns(tableInfo);
            var nonRelationalColumns = GetNonRelationalColumns(tableInfo, foreignKeyColumns);
            WriteReferences(tableInfo, textWriter);
            WriteNameSpace(nameSpace, textWriter);
            WriteBlockStart(0, textWriter);
            WriteTypeDeclaration(tableInfo, textWriter);
            WriteBlockStart(1, textWriter);
            WriteMemberDeclarations(foreignTables, textWriter);
            WriteNonRelationalProperties(nonRelationalColumns, textWriter);
            WriteOneToManyProperties(foreignTables, textWriter);
            WriteOneToOneProperties(GetOneToOneForeignkeys(tableInfo), textWriter);
            WriteManyToOneProperties(GetManyToOneForeignKeys(tableInfo), textWriter);
            if (generateEquals)
            {
                WriteGetHashCode(tableInfo, textWriter);
                WriteEquals(tableInfo, textWriter);
            }
            WriteBlockEnd(1, textWriter);
            WriteBlockEnd(0, textWriter);
            
            textWriter.Flush();
        }


        private void WriteGetHashCode(TableInfo tableInfo, TextWriter textWriter)
        {            
            textWriter.WriteLine();
            var primaryKeyColumns = tableInfo.PrimaryKey.Columns;
            string transientCheck = null;
            string hashCodeGeneration = null;
            textWriter.WriteLine(2, "public override int GetHashCode()");
            WriteBlockStart(2, textWriter);
            foreach (var primaryKeyColumn in primaryKeyColumns)
            {
                string defaultValue = GetDefaultValueAsString(primaryKeyColumn.DataType);
                if (transientCheck != null)
                    transientCheck += " && ";
                transientCheck += string.Format("({0} == {1})", primaryKeyColumn.ColumnName, defaultValue);
                if (hashCodeGeneration != null)
                    hashCodeGeneration += " ^ ";
                hashCodeGeneration += string.Format("{0}.GetHashCode()", primaryKeyColumn.ColumnName);
            }
            textWriter.WriteLine(3, "if ({0})", transientCheck);            
            textWriter.WriteLine(4, "return base.GetHashCode();");
            textWriter.Write(3, "return {0};", hashCodeGeneration);
            textWriter.WriteLine();
            WriteBlockEnd(2, textWriter);
        }


        private void WriteEquals(TableInfo tableInfo, TextWriter textWriter)
        {
            textWriter.WriteLine();
            string typeName = GetTypeName(tableInfo);
            var primaryKeyColumns = tableInfo.PrimaryKey.Columns;
            if (primaryKeyColumns.Count == 0)
                return;
            textWriter.WriteLine(2,"public override bool Equals(object obj)");
            WriteBlockStart(2,textWriter);
            textWriter.WriteLine(3,"{0} otherObject = obj as {0};", typeName);
            textWriter.WriteLine(3, "if (otherObject == null) return false;");

            string transientCheck = null;
            string returnStatement = null;
            
            foreach (var primaryKeyColumn in primaryKeyColumns)
            {
                string defaultValue = GetDefaultValueAsString(primaryKeyColumn.DataType);
                if (transientCheck != null)
                    transientCheck += " && ";
                    transientCheck += string.Format("({0} == {1} && otherObject.{0} == {1})",
                                                    primaryKeyColumn.ColumnName, defaultValue);
                
                if (returnStatement != null)
                    returnStatement += " && ";
                returnStatement += string.Format("({0} == otherObject.{0})", primaryKeyColumn.ColumnName);
            }
            textWriter.WriteLine(3,"if ({0})", transientCheck);            
            textWriter.WriteLine(4,"return base.Equals(obj);");
            
            
            textWriter.Write(3, "return {0};",returnStatement);

            textWriter.WriteLine();
            WriteBlockEnd(2, textWriter);
        }

        private string GetDefaultValueAsString(Type type)
        {
            var defaultvalue = GetDefaultValue(type);
            if (defaultvalue == null)
                return null;
       
            return defaultvalue.ToString();
       
        }
        


        private object GetDefaultValue(Type type)
        {
            object obj = type.IsValueType ? Activator.CreateInstance(type) : null;
            return obj;
        }

        

        private static IEnumerable<ForeignKeyInfo> GetManyToOneForeignKeys(TableInfo tableInfo)
        {
            return tableInfo.ForeignKeys;
        }

        private static IEnumerable<ForeignKeyInfo> GetOneToOneForeignkeys(TableInfo tableInfo)
        {
            var foreignKeyCandidates = GetForeignKeysReferencing(tableInfo);
            return foreignKeyCandidates.Where(ForeignKeyIsAlsoPrimaryKey);
        }





        protected virtual void WriteBlockStart(int indentLevel, TextWriter textWriter)
        {
            textWriter.WriteLine(indentLevel, "{");
        }

        protected virtual void WriteBlockEnd(int indentLevel, TextWriter textWriter)
        {
            textWriter.WriteLine(indentLevel, "}");
        }

        protected virtual void WriteManyToOneProperties(IEnumerable<ForeignKeyInfo> foreignKeys, TextWriter textWriter)
        {
            foreignKeys.ForEach(fk => WriteManyToOneProperty(fk, textWriter));
        }

        protected virtual void WriteManyToOneProperty(ForeignKeyInfo foreignKey, TextWriter textWriter)
        {
            var properyName = GetReferentialPropertyName(foreignKey);
            textWriter.WriteLine();
            textWriter.WriteLine(2, "/// <summary>");
            textWriter.WriteLine(2, "/// Gets or sets the {0}", ToCommentFriendlyText(properyName));
            textWriter.WriteLine(2, "/// </summary>");
            WriteDataMemberAttribute(2,textWriter);
            textWriter.WriteLine(2, "public virtual {0} {1} {{ get; set;}}", GetTypeName(foreignKey.PrimaryTable), GetReferentialPropertyName(foreignKey));
        }


        protected virtual void WriteOneToOneProperties(IEnumerable<ForeignKeyInfo> foreignKeys, TextWriter textWriter)
        {
            foreignKeys.ForEach(fk => WriteOneToOneProperty(fk, textWriter));
        }

        protected virtual void WriteOneToOneProperty(ForeignKeyInfo foreignKey, TextWriter textWriter)
        {
            var properyName = GetTypeName(foreignKey.Table);
            textWriter.WriteLine();
            textWriter.WriteLine(2, "/// <summary>");
            textWriter.WriteLine(2, "/// Gets or sets the {0}", ToCommentFriendlyText(properyName));
            textWriter.WriteLine(2, "/// </summary>");
            WriteDataMemberAttribute(2, textWriter);
            textWriter.WriteLine(2, "public virtual {0} {1} {{ get; set;}}", GetTypeName(foreignKey.Table), properyName);
        }

        private void WriteDataMemberAttribute(int indentLevel, TextWriter textWriter)
        {
            //_memberIndex++;
            //textWriter.WriteLine(indentLevel, "[DataMember(Order = {0})]",_memberIndex);
        }

        protected virtual string GetReferentialPropertyName(ForeignKeyInfo foreignKeyInfo)
        {
            string propertyName;

            var primaryTableTypename = GetTypeName(foreignKeyInfo.PrimaryTable);

            //Handle non-composite keys
            if (foreignKeyInfo.Columns.Count == 1)
            {
                var foreignKeyColumnInfo = foreignKeyInfo.Columns.First();
                var foreignKeyColumnName = foreignKeyColumnInfo.ForeignColumnInfo.ColumnName;
                var primaryKeyColumnName = foreignKeyColumnInfo.PrimaryColumnInfo.ColumnName;

                if (foreignKeyColumnName != primaryKeyColumnName)
                {
                    if (foreignKeyColumnName.Contains(primaryKeyColumnName))
                        propertyName = foreignKeyColumnName.Replace(primaryKeyColumnName, primaryTableTypename);
                    else
                        propertyName = primaryTableTypename;
                }
                else
                {
                    propertyName = primaryTableTypename;                   
                }
            }
            else
            {
                propertyName = primaryTableTypename;
            }
            if (foreignKeyInfo.Table == foreignKeyInfo.PrimaryTable)
                propertyName = string.Format("Parent{0}", propertyName);            
            return propertyName;
        }


        protected virtual void WriteOneToManyProperties(IEnumerable<TableInfo> foreignTables, TextWriter textWriter)
        {
            foreignTables.ForEach(ft => WriteOneToManyProperty(ft, textWriter));
        }

        protected virtual void WriteOneToManyProperty(TableInfo tableInfo, TextWriter textWriter)
        {
            textWriter.WriteLine();
            textWriter.WriteLine(2, "/// <summary>");
            textWriter.WriteLine(2, "/// Gets the {0}", ToCommentFriendlyText(GetTableName(tableInfo)));
            textWriter.WriteLine(2, "/// </summary>");

            textWriter.WriteLine(2, "public virtual IList<{0}> {1} ", GetTypeName(tableInfo),
                                  GetTableName(tableInfo));
            textWriter.WriteLine(2, "{");
            textWriter.WriteLine(3, "get {{ return {0}; }}", ToMemberName(GetTableName(tableInfo)));
            textWriter.WriteLine(2, "}");
        }

        protected virtual void WriteNonRelationalProperties(IEnumerable<ColumnInfo> columns, TextWriter textWriter)
        {
            columns.ForEach(c => WriteNonRelationalProperty(c, textWriter));
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="columnInfo"></param>
        /// <param name="textWriter"></param>
        protected virtual void WriteNonRelationalProperty(ColumnInfo columnInfo, TextWriter textWriter)
        {
            textWriter.WriteLine();
            textWriter.WriteLine(2, "/// <summary>");
            textWriter.WriteLine(2, "/// Gets or sets the {0}", ToCommentFriendlyText(columnInfo.ColumnName));
            textWriter.WriteLine(2, "/// </summary>");
            WriteDataMemberAttribute(2, textWriter);
            textWriter.WriteLine(2, "public virtual {0} {1} {{ get; set; }}", GetDataTypeName(columnInfo.DataType), columnInfo.ColumnName);
        }

        protected virtual void WriteReferences(TableInfo tableInfo, TextWriter textWriter)
        {
            textWriter.WriteLine(0, "using System;");
            textWriter.WriteLine(0, "using System.Runtime.Serialization;");
            textWriter.WriteLine(0, "using System.Collections.Generic;");
            textWriter.WriteLine(0, "");
        }

        protected virtual void WriteNameSpace(string nameSpace, TextWriter textWriter)
        {
            textWriter.WriteLine(0, "namespace {0}", nameSpace);
        }

        protected virtual void WriteMemberDeclarations(IEnumerable<TableInfo> foreignTables, TextWriter textWriter)
        {
            if (foreignTables.Count() == 0)
                return;
            textWriter.WriteLine(2, "#region Private Members");
            textWriter.WriteLine("");
            foreignTables.ForEach(ft => WriteMemberDeclaration(ft, textWriter));
            textWriter.WriteLine("");
            textWriter.WriteLine(2, "#endregion");
        }


        protected virtual void WriteTypeDeclaration(TableInfo tableInfo, TextWriter textWriter)
        {
            textWriter.WriteLine();
            textWriter.WriteLine(1, "/// <summary>");
            textWriter.WriteLine(1, "/// A class that represents {0}.", ToCommentFriendlyText(GetTableName(tableInfo)));
            textWriter.WriteLine(1, "/// </summary>");
            textWriter.WriteLine(1, "[Serializable]");
            textWriter.WriteLine(1, "public class {0}", GetTypeName(tableInfo));
        }

        private static IEnumerable<TableInfo> GetOneToManyForeignTables(TableInfo tableInfo)
        {
            //Find the foreign keys in the foreign tables that references this table. 
            IEnumerable<ForeignKeyInfo> foreignTableForeignKeyCandidates =
                GetForeignKeysReferencing(tableInfo);

            //Exclude keys that represents a one to one relationship.
            var foreignTableForeignKeys = foreignTableForeignKeyCandidates
                .Where(fk => !ForeignKeyIsAlsoPrimaryKey(fk));

            //Do not create a bidirectional relationship if the foreign table 
            //has more than one reference to this table.
            var foreignKeys = foreignTableForeignKeys.GroupBy(fk => fk.Table)
                .Where(g => g.Count() == 1).Select(g => g.First());

            var foreignTables = foreignKeys.Select(fk => fk.Table);

            return foreignTables;

        }




        private static IEnumerable<ForeignKeyInfo> GetForeignKeysReferencing(TableInfo tableInfo)
        {
            return tableInfo.Relations.Select(r => r.ForeignKey)
                .Except(tableInfo.ForeignKeys);
        }

        private static bool ForeignKeyIsAlsoPrimaryKey(ForeignKeyInfo foreignKeyInfo)
        {
            var primaryKeyColumns = foreignKeyInfo.Columns.Select(fkc => fkc.ForeignColumnInfo);
            var foreignKeysColumns = foreignKeyInfo.Table.PrimaryKey.Columns;
            return foreignKeysColumns.Except(primaryKeyColumns).Count() == 0;
        }


        private static IEnumerable<ColumnInfo> GetForeignKeyColumns(TableInfo tableInfo)
        {
            var foreignKeyColumns = tableInfo.ForeignKeys.SelectMany(fk => fk.Columns)
                .Select(fkc => fkc.ForeignColumnInfo).Except(tableInfo.PrimaryKey.Columns);
            return foreignKeyColumns;
        }


        private static IEnumerable<ColumnInfo> GetNonRelationalColumns(TableInfo tableInfo, IEnumerable<ColumnInfo> foreignKeyColumns)
        {
            var columns = tableInfo.Columns.Values.Except(foreignKeyColumns).OrderBy(c => c.OrdinalPosition);
            return columns;
        }



        protected virtual void WriteMemberDeclaration(TableInfo foreignTable, TextWriter textWriter)
        {
            var memberName = ToMemberName(GetTableName(foreignTable));
            var typeName = GetTypeName(foreignTable);
            WriteDataMemberAttribute(2, textWriter);
            textWriter.WriteLine(2, "private IList<{0}> {1} = new List<{0}>();", typeName, memberName);
        }



        protected virtual string GetTypeName(TableInfo tableInfo)
        {
            return GetTableName(tableInfo).ToSingular();
        }

        private static string ToMemberName(string value)
        {
            return string.Format("_{0}", value.Substring(0, 1).ToLower() + value.Substring(1));
        }

        protected virtual string GetTableName(TableInfo tableInfo)
        {
            return tableInfo.TableName.Replace(" ", "");
        }

        protected static string GetDataTypeName(Type type)
        {

            if (type.IsGenericType && type.GetGenericTypeDefinition().Equals(typeof(Nullable<>)))
            {
                return (ToIntegralName(Nullable.GetUnderlyingType(type)) + "?");
            }

            return ToIntegralName(type);

        }


        protected static string ToIntegralName(Type type)
        {
            switch (type.Name)
            {
                case "Boolean":
                    return "bool";
                case "Single":
                    return "float";
                case "Int16":
                    return "short";
                case "UInt16":
                    return "ushort";
                case "Int32":
                    return "int";
                case "UInt32":
                    return "uint";
                case "Int64":
                    return "long";
                case "UInt64":
                    return "ulong";
                case "DateTime":
                    return type.Name;
                case "XElement":
                    return type.Name;
            }
            return type.Name.ToLower();
        }

        private string ToCommentFriendlyText(string propertyName)
        {
            var output = System.Text.RegularExpressions.Regex.Replace(propertyName, "([A-Z][A-Z]*)", " $1",
                    System.Text.RegularExpressions.RegexOptions.Compiled).Trim();
            return output;
        }
    }


}
